/***************************************************************
*  Copyright (c) 2007 by GroupMe! Team (www.groupme.net)
*  All rights reserved.
*
*  This file is part of the GroupMe! Project. Source code of 
*  this project is closed and redistribution of this code is
*  prohibited. 
*  
*  Contact: http://www.groupme.net
*
*  This copyright notice MUST APPEAR in all copies of the file!
***************************************************************/
package net.groupme.search;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import net.groupme.controller.model.HtmlFormattedElement;
import net.groupme.exceptions.UpdateFailedException;
import net.groupme.exceptions.search.SearchEngineException;
import net.groupme.gui.constants.VIEW_VOCABULARY;
import net.groupme.model.Group;
import net.groupme.model.MODEL_CONSTANTS;
import net.groupme.model.ObjectFactory;
import net.groupme.model.Resource;
import net.groupme.model.Tag;
import net.groupme.model.User;
import net.groupme.model.cache.CachedObject;
import net.groupme.model.cache.CachedObjectType;
import net.groupme.search.strategy.model.WeightedTag;
import net.groupme.utils.StringUtilities;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * created on Jul 3, 2007
 * 
 * This class manages search requests. It acts like a lyer between the actual controller
 * and the {@link GroupMeSearchEngine}.
 * 
 * @author Fabian Abel, <a href="mailto:abel@l3s.de">abel@l3s.de</a>
 * @author last edited by: $Author: nicole $
 * 
 * @version $Revision: 1.17 $ $Date: 2008-07-25 11:17:27 $
 */
public class SearchManager implements ApplicationContextAware{


	/** Logger for this class and subclasses */
	protected final Log logger = LogFactory.getLog(getClass());
	
	/** the suting groups of the last search */
	private List<Group> suitingGroups = null;
	
	/** the suiting resources (and groups) of the last search */
	private List<Resource> suitingResources = null;
	
	/** the suiting and filtered resources (and groups) of the last search */
	private List<Resource> suitingFilteredResources = null;
	
	/** indicates wheather the last search was for Groups, Resources, both, ...*/
	private String lastSearchFor = null;
	
	/** cached overall tag cloud */
	private CachedObject cachedOverallTagCloud = ObjectFactory.getCachedObject(CachedObjectType.TAG_CLOUD_WHOLE, null, null);
	
	
	/** the application context */
	private ApplicationContext applicationContext = null;
	
	/**
	 * Set the ApplicationContext that this object runs in. 
	 * @param applicationContext the current {@link ApplicationContext} 
	 */
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;		
	}

	/**
	 * This method returns the suitingGroups of a SearchManager object.
	 * @return the suitingGroups
	 */
	public List<Group> getSuitingGroups() {
		return suitingGroups;
	}

	/**
	 * This method sets the suitingGroups of a SearchManager object.
	 * @param suitingGroups the suitingGroups to set
	 */
	public void setSuitingGroups(List<Group> suitingGroups) {
		this.suitingGroups = suitingGroups;
	}

	/**
	 * This method returns the suitingResources of a SearchManager object.
	 * @return the suitingResources
	 */
	public List<Resource> getSuitingResources() {
		return suitingResources;
	}

	/**
	 * This method sets the suitingResources of a SearchManager object.
	 * @param suitingResources the suitingResources to set
	 */
	public void setSuitingResources(List<Resource> suitingResources) {
		this.suitingResources = suitingResources;
	}
	
	/**
	 * This method returns the suitingFilteredResources of a SearchManager object.
	 * @return the suitingResources
	 */
	public List<Resource> getSuitingFilteredResources() {
		return suitingFilteredResources;
	}
	
	/**
	 * This method sets the suitingFilteredResources of a SearchManager object.
	 * @param suitingResources the suitingResources to set
	 */
	public void setSuitingFilteredResources(List<Resource> suitingResources){
		this.suitingFilteredResources = suitingResources;
	}

	/**
	 * This method returns the lastSearchFor of a SearchManager object.
	 * @return the lastSearchFor
	 */
	public String getLastSearchFor() {
		return lastSearchFor;
	}

	/**
	 * This method sets the lastSearchFor indicator. It should be set 
	 * before a search is executed (via SearchAjaxController).
	 * 
	 * @param lastSearchFor the lastSearchFor to set
	 */
	public void setLastSearchFor(String lastSearchFor) {
		this.lastSearchFor = lastSearchFor;
	}
	
	/**
	 * Resets the Search History.
	 */
	public void resetSearchHistory(){
		this.lastSearchFor = null;
		this.suitingGroups = null;
		this.suitingResources = null;
	}

	/**
	 * This method returns the cachedOverallTagCloud of a SearchManager object.
	 * @return the cachedOverallTagCloud or <code>null</code> if no tag cloud is cached
	 */
	public HtmlFormattedElement getCachedTagCloud() {
		if(cachedOverallTagCloud == null){
			return null;
		}
		return (HtmlFormattedElement) cachedOverallTagCloud.getChachedObject();
	}

	/**
	 * This method sets the cachedOverallTagCloud of a SearchManager object.
	 * @param cachedTagCloud theoverall tag cloud to cache
	 */
	public void setCachedTagCloud(HtmlFormattedElement cachedTagCloud) {
		if(this.cachedOverallTagCloud == null){
			this.cachedOverallTagCloud = ObjectFactory.createCachedObject(cachedTagCloud, CachedObjectType.TAG_CLOUD_WHOLE);
		}else{
			try {
				this.cachedOverallTagCloud.setChachedObject(cachedTagCloud);
			} catch (UpdateFailedException e) {
				e.printStackTrace();
				logger.error("Failed to cache the tag cloud.", e);
			}
		}
	}

	/**
	 * This method returns the cached user tag cloud of a SearchManager object.
	 * @return the cachedOverallTagCloud
	 */
	public String getCachedUserTagCloud() {
		return null;
	}

	/**
	 * This method sets the cached user tag cloud of a SearchManager object.
	 * @param cachedTagCloud the user tag cloud to cache
	 */
	public void setCachedUserTagCloud(String cachedTagCloud) {
		//this.cachedUserTagCloud = cachedTagCloud;
	}
	
	/**
	 * This method simply redirects the search request to the {@link GroupMeSearchEngine}.
	 * Thus, it calls {@link GroupMeSearchEngine#searchForGroups(Collection, boolean)}.
	 * 
	 * @param tags the tags to search with
	 * @param allTags indicates wheater all tags have to be related with a resulting group (<code>true</code>)
	 * or only any of the given tags (<code>false</code>)
	 * @return an ordered list of suiting {@link Group} instances
	 * @throws SearchEngineException thrown if errors occur during the internal search
	 */
	public List<Group> searchForGroups(String keywords, boolean allTags) throws SearchEngineException{
		Collection<Tag> tags = getTags(keywords);
		GroupMeSearchEngine searchEngine = (GroupMeSearchEngine) applicationContext.getBean(VIEW_VOCABULARY.GROUPME_SEARCH_ENGINE, GroupMeSearchEngine.class);
		
		//groupme-logging:
		

		suitingGroups = searchEngine.searchForGroups(tags, allTags);
		return suitingGroups;
	}
	
	/**
	 * This method simply redirects the search request to the {@link GroupMeSearchEngine}.
	 * Thus, it calls {@link GroupMeSearchEngine#searchForResources(Collection, boolean)}.
	 * 
	 * @param tag the tags to search with
	 * @param allTags indicates wheater all tags have to be related with a resulting group (<code>true</code>)
	 * or only any of the given tags (<code>false</code>)
	 * @return an ordered list of suiting {@link Group} instances
	 * @param noGroups as {@link Group} instances are also {@link Resource} one can explicitly exclude {@link Group} instances by
	 * if this parameter is <code>false</code>
	 * @throws SearchEngineException thrown if errors occur during the internal search
	 */
	public List<Resource> searchForResources(String keywords,  boolean allTags, boolean noGroups) throws SearchEngineException{
		Collection<Tag> tags = getTags(keywords);
		GroupMeSearchEngine searchEngine = (GroupMeSearchEngine) applicationContext.getBean(VIEW_VOCABULARY.GROUPME_SEARCH_ENGINE, GroupMeSearchEngine.class);
		
		suitingResources = searchEngine.searchForResources(tags, allTags, noGroups);
		return suitingResources;
	}

	/**
	 * Small helper function to extract {@link Tag} instances from a given keyword query string.
	 * @param keywords the keywords as a string which may look like this: <i>"Semantic Web" OWL RDF</i>
	 * @return a collection of separated tags which may look like this: <i>[Semantic Web, OWL, RDF]</i>
	 * @throws SearchEngineException thrown if creation/receiving of {@link Tag} instance fails
	 */
	private Collection<Tag> getTags(String keywords) throws SearchEngineException{
		Collection<String> singleKeywords = StringUtilities.extractTags(keywords, true);
		Collection<Tag> tags = new HashSet<Tag>();
		for(String keyword: singleKeywords){
			Tag actualTag = ObjectFactory.getTag(keyword);
			if(actualTag != null){
				tags.add(actualTag);
			}
		}
		return tags;
	}

	/**
	 * Returns the actual number of search results (#suitingResources + #suitingGroups).
	 * @return the actual number of search results (#suitingResources + #suitingGroups).
	 */
	public int getActualNumberOfResults() {
		int numberOfResults = 0;
		if(suitingGroups != null){
			numberOfResults += suitingGroups.size();
		}
		if(suitingResources != null){
			numberOfResults += suitingResources.size();
		}
		return numberOfResults;
	}
	
	/**
	 * This method returns the tag cloud of a group which is an ordered list of {@link WeightedTag} instances. 
	 * Ordering is done according to the number of occurences in the given period within the group. 
	 *  
	 * @param group the group about which the tag cloud should be 
	 * @param topK to specify the maximum number of tags that are contained in the tag cloud. If <code>topK</code> = 50 then the 50 most
	 * important tags are returned (50 most popular tags). If <code>topK == null</code> then all tags will be returned (this may take very long).
	 * @return an ordered list of {@link WeightedTag} instances according to the number of occurences in the specified period of time
	 */
	public List<WeightedTag> computeTagCloud(Group group, Integer topK){
		GroupMeSearchEngine searchEngine = (GroupMeSearchEngine) applicationContext.getBean(VIEW_VOCABULARY.GROUPME_SEARCH_ENGINE, GroupMeSearchEngine.class);
		return searchEngine.getTagCloud(group, topK);
	}
	
	/**
	 * This method returns an ordered list of {@link WeightedTag} instances. Ordering is done according
	 * to the number of occurences in the given period. Such a list builds the base for <i>Tag Clouds</i>.
	 * This method simply calls {@link GroupMeSearchEngine#getTagCloud(Calendar, Calendar, User)}
	 * 
	 * @param beginOfPeriod start of period. If this is <code>null</code> then all tag occurences that have 
	 * a date lesser than <i>endOfPeriod</i> are considered. 
	 * @param endOfPeriod end of period. If this is <code>null</code> then all tag occurences that have 
	 * a date greater than <i>beginOfPeriod</i> are considered. 
	 * @param user the user who tagged the resources. If this is <code>null</code> then tag occurences of all 
	 * users are considerd. 
	 * @param topK to specify the maximum number of tags that are contained in the tag cloud. If <code>topK</code> = 50 then the 50 most
	 * important tags are returned (50 most popular tags). If <code>topK == null</code> then all tags will be returned (this may take very long).
	 * @return an ordered list of {@link WeightedTag} instances according to the number of occurences in the specified period of time
	 */
	public List<WeightedTag> computeTagCloud(Calendar beginOfPeriod, Calendar endOfPeriod, User user, Integer topK){
		GroupMeSearchEngine searchEngine = (GroupMeSearchEngine) applicationContext.getBean(VIEW_VOCABULARY.GROUPME_SEARCH_ENGINE, GroupMeSearchEngine.class);
		return searchEngine.getTagCloud(beginOfPeriod, endOfPeriod, user, topK);
	}
	/**
	 * Returns an ordered list of groups that are similar to the given group. Similarity is
	 * determined based on tags. In detail: This method simply performs a search for groups
	 * with the keywords of the given group and returns the first x results (where x is the
	 * maxNumberOfSimilarGroups).
	 * 
	 * @param group the group for which similar groups should be determined
	 * @param maxNumberOfSimilarGroups the maximum number of similar groups that should be returned
	 * @return an ordered list of groups that are similar to the given group
	 */
	public List<Group> getSimilarGroups(Group group, int maxNumberOfSimilarGroups){
		GroupMeSearchEngine searchEngine = (GroupMeSearchEngine) applicationContext.getBean(VIEW_VOCABULARY.GROUPME_SEARCH_ENGINE, GroupMeSearchEngine.class);

		List<Group> suitingGroups = searchEngine.searchForGroups(group.getTags(), false);
		List<Group> resultGroups = new ArrayList<Group>();
		if (suitingGroups != null) {
			for (int i = 0; i < suitingGroups.size()
					&& i < maxNumberOfSimilarGroups; i++) {
				if(!group.equals(suitingGroups.get(i))){
					resultGroups.add(suitingGroups.get(i));
				}
			}
		}
		return resultGroups;
	}
	
	/**
	 * Returns an ordered list of groups that are similar to the given group whereas only those groups are returned
	 * which are <strong>not</strong> owned by the given user. Similarity is
	 * determined based on tags. In detail: This method simply performs a search for groups
	 * with the keywords of the given group and returns the first x results (where x is the
	 * maxNumberOfSimilarGroups).
	 * 
	 * @param group the group for which similar groups should be determined
	 * @param user the user for whom the "recommendations" should be computed
	 * @param maxNumberOfSimilarGroups the maximum number of similar groups that should be returned
	 * @return an ordered list of groups that are similar to the given group
	 */
	public List<Group> getSimilarGroups(Group group, User user, int maxNumberOfSimilarGroups){
		GroupMeSearchEngine searchEngine = (GroupMeSearchEngine) applicationContext.getBean(VIEW_VOCABULARY.GROUPME_SEARCH_ENGINE, GroupMeSearchEngine.class);

		List<Group> suitingGroups = searchEngine.searchForGroups(group.getTags(), false);
		List<Group> resultGroups = new ArrayList<Group>();
		if (suitingGroups != null) {
			for (int i = 0; i < suitingGroups.size()
					&& i < maxNumberOfSimilarGroups; i++) {
				if(!suitingGroups.get(i).getUsers(MODEL_CONSTANTS.ROLE_OWNER).contains(user)){
					resultGroups.add(suitingGroups.get(i));
				}
			}
		}
		return resultGroups;
	}
}
